package org.lestr.secuel.m3ds.importation.objets3D;

import java.io.File;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;
import java.util.StringTokenizer;

import java.util.logging.Level;
import java.util.logging.Logger;
import org.lestr.secuel.m3ds.IUsine;
import org.lestr.secuel.m3ds.importation.objets2D.ImageImportException;
import org.lestr.secuel.m3ds.importation.objets2D.ImageImportateurs;
import org.lestr.secuel.m3ds.maths.IPoint3d;
import org.lestr.secuel.m3ds.maths.IPoint2d;
import org.lestr.secuel.m3ds.objets3D.Materiau;
import org.lestr.secuel.m3ds.objets3D.IObjet3d;
import org.lestr.secuel.m3ds.objets3D.ITriangle;

public class ImportateurWavefront implements ImportateurObjet3d {


    // Déclaration des attributs
    private String racine;


    private StringTokenizer compilateur;


    private ArrayList<IPoint3d> v;


    private ArrayList<ITriangle> triangles;


    private ArrayList<IPoint2d> vt;


    private ArrayList<IPoint2d> coordonnees;


    private ArrayList<IPoint3d> vn;


    private ArrayList<IPoint3d> normales;


    private IUsine usine;


    private String nom;


    private Collection<IObjet3d> objets;


    private MTLImport mtlImport;


    private Map<String, Materiau> matériaux;


    private Materiau matériauCourant;


    private Materiau matériauStandard;


    private Map<ITriangle, Materiau> objetCourantMatériaux;


    /** Retourne l'extension des fichiers du format de fichier importé */
    @Override
    public String getFormatExtension() {

        return "obj";

    }// FIN retFormatExtension


    /** Retourne le nom du format de fichier importé */
    @Override
    public String getFormatNom() {

        return "Wavefront Object file";

    }// FIN retFormatNom


    /** Importe un fichier OBJ */
    @Override
    public Collection<IObjet3d> importer(File fichier,
                                         IUsine usine) throws ExceptionImportObjet3d {

        // Mémorise l'usine
        this.usine = usine;

        // Initialisations
        racine = fichier.getParent();
        v = new ArrayList<IPoint3d>();
        triangles = new ArrayList<ITriangle>();
        vt = new ArrayList<IPoint2d>();
        nom = "Nouvel Objet";
        objets = new ArrayList<IObjet3d>();
        coordonnees = new ArrayList<IPoint2d>();
        vn = new ArrayList<IPoint3d>();
        normales = new ArrayList<IPoint3d>();
        mtlImport = new MTLImport();
        matériaux = new HashMap<String, Materiau>();
        matériauCourant = null;
        matériauStandard = null;
        objetCourantMatériaux = new HashMap<ITriangle, Materiau>();

        // Construit un scanneur avec le fichier source
        Scanner scan;
        try {
            scan = new Scanner(fichier);
        } catch (FileNotFoundException e) {
            return null;
        }

        // Parcours les lignes du fichier
        while (scan.hasNext()) {

            // Récupère la nouvelle ligne
            String ligne = scan.nextLine();

            // Fait l'analyse lexicale
            compilateur = new StringTokenizer(ligne);

            // Si la ligne n'est pas vide
            if (compilateur.hasMoreTokens()) {

                // Récupère l'ordre
                String ordre = compilateur.nextToken();

                // Si c'est un nouvel objet
                if (ordre.equals("o"))
                    objet();

                // Si c'est un nouvellesPosition
                if (ordre.equals("v"))
                    point3D();

                // Si c'est un point2d de texturage
                if (ordre.equals("vt"))
                    texture();

                // Si c'est un point2d de texturage
                if (ordre.equals("vn"))
                    normale();

                // Si c'est un triangle
                if (ordre.equals("f"))
                    triangle();

                // Si c'est de fichiers de matérieux
                if (ordre.equals("mtllib"))
                    matériauxFichiers();

                // Si c'est un matériau
                if (ordre.equals("usemtl"))
                    matériau();

            }

        }

        // Calcul le dernier objet
        convertir();

        // Retourne le résultat
        return objets;

    }// FIN importer


    /** Calcul l'objet courant */
    private void convertir() {

        // Construit l'objet courant
        IObjet3d objet = usine.objet3D();
        objet.ajouterTriangles(triangles);
        objet.définirNom(nom);
        nom = "Nouvel Objet";

        // Matériaux standard s'il y en a
        if (matériauStandard != null) {
            objet.obtenirApparence().obtenirMatériauStandard().définirCouleurAmbiante(matériauStandard.obtenirRougeAmbiant(),
                                                                            matériauStandard.obtenirVertAmbiant(),
                                                                            matériauStandard.obtenirBleuAmbiant());
            objet.obtenirApparence().obtenirMatériauStandard().définirCouleurDiffuse(matériauStandard.obtenirRougeDiffue(),
                                                                           matériauStandard.obtenirVertDiffue(),
                                                                           matériauStandard.obtenirBleuDiffue());
            objet.obtenirApparence().obtenirMatériauStandard().définirCouleurSpéculaire(matériauStandard.obtenirRougeSpéculaire(),
                                                                              matériauStandard.obtenirVertSpéculaire(),
                                                                              matériauStandard.obtenirBleuSpéculaire());
            objet.obtenirApparence().obtenirMatériauStandard().définirCouleurEmissive(matériauStandard.obtenirRougeEmissif(),
                                                                            matériauStandard.obtenirVertEmissif(),
                                                                            matériauStandard.obtenirBleuEmissif());
            objet.obtenirApparence().obtenirMatériauStandard().définirImage(matériauStandard.obtenirImage());
        }

        // Matériaux spécifiques
        for (ITriangle triangle : objetCourantMatériaux.keySet())
            objet.obtenirApparence().définirMatériau(objetCourantMatériaux.get(triangle), triangle);

        // Coordonnées de texture s'il y a
        if (!coordonnees.isEmpty()) {
            objet.obtenirApparence().définirTexturageAutomatique(false);
            objet.obtenirApparence().setCoordonnées(coordonnees);
        }

        // Normales s'il y a
        if (!normales.isEmpty()) {
            objet.obtenirApparence().définirNormalesAutomatiques(false);
            objet.obtenirApparence().définirNormales(normales);
        }

        // Ajoute l'objet courant au résultat
        objets.add(objet);

    }// FIN convertir


    /** Compile un matériau */
    private void matériau() {

        // Si c'est le premier matériaux, le note
        boolean premierMatériau = matériauCourant == null;

        // Mémorise le nouveau matériau courant
        matériauCourant = matériaux.get(compilateur.nextToken());

        // Si c'est le premier matériaux, le défini comme matériaux par défaut
        if (premierMatériau)
            matériauStandard = matériauCourant;

    }// FIN matériau


    /** Compile des fichiers de matériaux */
    private void matériauxFichiers() {

        // Parcours les fichiers de matériaux
        while (compilateur.hasMoreTokens()) {

            // Récupère le fichier de matériau courant
            File materiauFichier = new File(racine + "/" + compilateur.nextToken());

            // Compile le matériau
            mtlImport.importer(materiauFichier);

        }

    }// FIN matériauxFichiers


    /** Compile un nouvel objet */
    private void objet() {

        // Calcul l'objet précédent
        convertir();

        // Vide les triangles
        triangles.clear();
        coordonnees.clear();
        normales.clear();
        objetCourantMatériaux.clear();

        // Récupère le nom
        nom = compilateur.nextToken();

    }// FIN objet


    /** Compile un point2d pour texture */
    private void texture() {

        // Contruit le nouveau point
        IPoint2d point2D = usine.point2d();

        // Mémorise ses coordonnées
        point2D.définirX(Float.parseFloat(compilateur.nextToken()));
        point2D.définirY(Float.parseFloat(compilateur.nextToken()));

        // L'ajoute au tableau des points
        vt.add(point2D);

    }// FIN texture


    /** Compile un point2d pour texture */
    private void normale() {

        // Contruit le nouveau point
        IPoint3d point3D = usine.point3d();

        // Mémorise ses coordonnées
        point3D.définirX(Float.parseFloat(compilateur.nextToken()));
        point3D.définirY(Float.parseFloat(compilateur.nextToken()));
        point3D.définirZ(Float.parseFloat(compilateur.nextToken()));

        // L'ajoute au tableau des points
        vn.add(point3D);

    }// FIN texture


    /** Compile un nouvellesPosition */
    private void point3D() {

        // Contruit le nouveau point
        IPoint3d point3D = usine.point3d();

        // Mémorise ses coordonnées
        point3D.définirX(Double.parseDouble(compilateur.nextToken()));
        point3D.définirY(Double.parseDouble(compilateur.nextToken()));
        point3D.définirZ(Double.parseDouble(compilateur.nextToken()));

        // L'ajoute au tableau des points
        v.add(point3D);

    }// FIN nouvellesPosition


    /** Compile un triangle */
    private void triangle() {

        // Contruit le nouveau triangle
        ITriangle triangle = usine.triangle();

        // Mémorise ses points
        triangle.obtenirSommet1().définirXYZ(sommet());
        triangle.obtenirSommet2().définirXYZ(sommet());
        triangle.obtenirSommet3().définirXYZ(sommet());

        // S'il y a un matériau courant de défini et différent de celui par défaut, le prend en compte
        if (matériauCourant != matériauStandard)
            objetCourantMatériaux.put(triangle, matériauCourant);

        // L'ajoute au tableau des triangles
        triangles.add(triangle);

        // S'il y a un point supplémentaire, ajoute un triangle
        if (compilateur.hasMoreTokens()) {

            // Contruit le nouveau triangle
            ITriangle triangle2 = usine.triangle();

            // Mémorise ses points
            triangle.obtenirSommet1().définirXYZ(triangle.obtenirSommet1());
            triangle.obtenirSommet2().définirXYZ(triangle.obtenirSommet2());
            triangle.obtenirSommet3().définirXYZ(sommet());

            // S'il y a un matériau courant de défini et différent de celui par défaut, le prend en compte
            if (matériauCourant != matériauStandard)
                objetCourantMatériaux.put(triangle2, matériauCourant);

            // L'ajoute au tableau des triangles
            triangles.add(triangle2);

        }

        // S'il y a un matériau courant de défini et différent de celui par défaut, le prend en compte
        if (matériauCourant != matériauStandard)
            objetCourantMatériaux.put(triangle, matériauCourant);

    }// FIN triangle


    /** Compile un sommet de triangle */
    private IPoint3d sommet() {

        // Récupère le code du sommet
        String élément = compilateur.nextToken();

        // S'il y a des coordonnées de texturage
        if (élément.indexOf('/') != -1) {

            String élémentb = élément.substring(élément.indexOf('/') + 1);
            élément = élément.substring(0, élément.indexOf('/'));

            // S'il y a des normales
            if (élémentb.indexOf("/") != -1) {

                String élément1c = élémentb.substring(élémentb.indexOf('/') + 1);
                élémentb = élémentb.substring(0, élémentb.indexOf('/'));

                // Mémorise les normales
                normales.add((IPoint3d) obtenirElément(vn, Integer.parseInt(élément1c) - 1));

            }

            // Mémorise les coordonnées de texturage
            coordonnees.add((IPoint2d) obtenirElément(vt, Integer.parseInt(élémentb) - 1));

        }

        // Retourne le sommet
        return (IPoint3d) obtenirElément(v, Integer.parseInt(élément) - 1);

    }// FIN sommet


    /** Retourne un élément dans une collection */
    private Object obtenirElément(ArrayList<?> collection,
                                  int i) {

        return i < 0 ? collection.get(collection.size() - i) : collection.get(i);

    }// FIN retElément


    /** Importateur de fichier MTL */
    private class MTLImport {


        // Déclaration des attributs
        private Materiau matériauEnConstruction;


        private StringTokenizer compilateur;


        /** Importe un fichier MTL */
        public void importer(File fichier) {

            // Initialisations
            matériauEnConstruction = null;

            // Construit un scanneur avec le fichier source
            Scanner scan;
            try {
                scan = new Scanner(fichier);
            } catch (FileNotFoundException e) {
                return;
            }

            // Parcours les lignes du fichier
            while (scan.hasNext()) {

                // Récupère la nouvelle ligne
                String ligne = scan.nextLine();

                // Fait l'analyse lexicale
                compilateur = new StringTokenizer(ligne);

                // Si la ligne n'est pas vide
                if (compilateur.hasMoreTokens()) {

                    // Récupère l'ordre
                    String ordre = compilateur.nextToken();

                    // Nouveau matériau
                    if (ordre.equals("newmtl"))
                        nouveauMateriau();

                    // Lumière ambiante
                    if (ordre.equals("Ka"))
                        ambiante();

                    // Lumière diffuse
                    if (ordre.equals("Kd"))
                        diffuse();

                    // Lumière spéculaire
                    if (ordre.equals("Ks"))
                        spéculaire();

                    // Image
                    if (ordre.equals("map_Ka") || ordre.equals("map_Kd") || ordre.equals("map_Ks"))
                        image();

                }

            }

        }// FIN importer


        /** Image pour la texture */
        private void image() {

            // Variables locales
            String élément;

            // Saute les options
            do
                élément = compilateur.nextToken();
            while (compilateur.hasMoreElements());

            // Importe l'image
            try {
                matériauEnConstruction.définirImage(ImageImportateurs.getSingleton().importer(new File(racine + "/" + élément), usine).iterator().next());
            } catch (ImageImportException ex) {
                Logger.getLogger(ImportateurWavefront.class.getName()).log(Level.SEVERE, null, ex);
            }


        }// FIN image


        /** Réaction à la lumière ambiante */
        private void ambiante() {

            matériauEnConstruction.définirCouleurAmbiante((int) (Float.valueOf(compilateur.nextToken()) * 255),
                                                          (int) (Float.valueOf(compilateur.nextToken()) * 255),
                                                          (int) (Float.valueOf(compilateur.nextToken()) * 255));

        }// FIN ambiante


        /** Réaction à la lumière diffuse */
        private void diffuse() {

            matériauEnConstruction.définirCouleurDiffuse((int) (Float.valueOf(compilateur.nextToken()) * 255),
                                                         (int) (Float.valueOf(compilateur.nextToken()) * 255),
                                                         (int) (Float.valueOf(compilateur.nextToken()) * 255));

        }// FIN diffuse


        /** Réaction à la lumière spéculaire */
        private void spéculaire() {

            matériauEnConstruction.définirCouleurSpéculaire((int) (Float.valueOf(compilateur.nextToken()) * 255),
                                                            (int) (Float.valueOf(compilateur.nextToken()) * 255),
                                                            (int) (Float.valueOf(compilateur.nextToken()) * 255));

        }// FIN spéculaire


        /** Nouveau matériau */
        private void nouveauMateriau() {

            // Initialise le matériau
            matériauEnConstruction = usine.matériau();

            // Le mémorise
            matériaux.put(compilateur.nextToken(), matériauEnConstruction);

        }// FIN nouveauMateriau


    }// FIN Classe MTLImport


}// FIN Classe


